﻿/*
 * Сделано в SharpDevelop.
 * Пользователь: max
 * Дата: 05.05.2012
 * Время: 18:14
 * 
 * 
 Copyright (c) 2012 Maksim Gordeev

Permission is hereby granted, free of charge, to any person obtaining a copy of this
software and associated documentation files (the "Software"), to deal in the Software
without restriction, including without limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or
substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
 */

using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;

namespace jellybird.core
{
	/*
	 * response 401 получаем, если в параметрах тела есть неамериканские буквы
	 * какая то херь с percent encoding
	 * Подправлено uri_encode - вроде работает нормально
	 * 
	 */
	
	/// <summary>
	/// Теперь не используем, но на всякий случай пусть будет пока
	/// 
	/// Инкапсулирует Authentication заголовок http запросов для твиттера.
	/// Можно не создавать новый для каждого запроса,
	/// изменяющиеся поля генерерируются каждый раз когда запрашивается
	/// CreateRequestHeader.
	/// Всё в соответствие с
	/// https://dev.twitter.com/docs/auth/authorizing-request
	/// </summary>
	public class OAuthHeader
	{
		public OAuthHeader()
		{
			init_content();
			init_constant_values();
		}
		
		public string ConsumerKey
		{
			get	{ return oauth_content[Constants.OAUTH_CONSUMER_KEY]; }
			set { oauth_content[Constants.OAUTH_CONSUMER_KEY]=value; }
		}
		
		public string AccessToken
		{
			get { return oauth_content[Constants.OAUTH_TOKEN_KEY]; }
			set { oauth_content[Constants.OAUTH_TOKEN_KEY] = value; }
		}
		
		string consumerSecret=string.Empty;
		public string ConsumerSecret
		{
			get { return consumerSecret; }
			set { consumerSecret = value; }
		}
		
		string tokenSecret=string.Empty;
		public string TokenSecret
		{
			get { return tokenSecret; }
			set { tokenSecret = value; }
		}
		
		/// <summary>
		/// Добавляет пользовательский параметр
		/// (типа oauth_callback)
		/// </summary>
		/// <param name="param_key">Имя</param>
		/// <param name="param_value">Значение</param>
		public void AddOAuthParam(string param_key,string param_value)
		{
			oauth_content.Add(param_key,param_value);
		}
		
		/// <summary>
		/// Удалить можно параметр, который добавлен
		/// методом AddOAuthParam
		/// </summary>
		/// <param name="param_key"></param>
		public void RemoveParam(string param_key)
		{
			if ((param_key==Constants.OAUTH_CONSUMER_KEY)||
			    (param_key==Constants.OAUTH_NONCE_KEY)||
			    (param_key==Constants.OAUTH_SIGANTURE_KEY)||
			    (param_key==Constants.OAUTH_SIGNATURE_METHOD_KEY)||
			    (param_key==Constants.OAUTH_TIMESTAMP_KEY)||
			    (param_key==Constants.OAUTH_TOKEN_KEY)||
			    (param_key==Constants.OAUTH_VERSION_KEY))
				throw new ArgumentOutOfRangeException("param_key","Can be only user added parameter");
			
			oauth_content.Remove(param_key);
		}
		
		public string this[string param_key]
		{
			get{return oauth_content[param_key];}
			set{oauth_content[param_key]=value;}
		}
		
		/// <summary>
		/// Генерируется заголовок Authentication OAuth для твиттера
		/// </summary>
		/// <param name="uri">Uri (нужен для создания подписи)</param>
		/// <param name="http_method">GET, POST и т.п. - для подписи</param>
		/// <param name="body">тело запроса как оно есть в запросе
		/// Если в подпись включать не надо - не надо и передавать.
		/// </param>
		/// <returns></returns>
		public string CreateRequestHeader
			(Uri uri,
			 string http_method,
			 string body)
		{
			/*
			 * http://tools.ietf.org/html/rfc5849
			 */
			
			//Заполняем oauth_content
			//Nonce:
			oauth_content[Constants.OAUTH_NONCE_KEY]=CreateNonceValue();
			//и timestamp
			oauth_content[Constants.OAUTH_TIMESTAMP_KEY]=Helper.UnixTimestamp().ToString();
			//и вычисляем подпись
			oauth_content[Constants.OAUTH_SIGANTURE_KEY]=create_signature(uri,http_method,body);
			
			//Теперь можно составить весь заголовок
			StringBuilder ret=new StringBuilder();
			ret.Append(Constants.OAUTH_ID);
			ret.Append(Constants.SPACE);
			foreach(KeyValuePair<string,string> pair in oauth_content)
			{
				//пропускаем пустые
				if(string.IsNullOrEmpty(pair.Value)) continue;
				ret.Append(pair.Key);
				ret.Append(Constants.OAUTH_KEY_VALUE_DELIMITER);
				ret.Append(Constants.OAUTH_QUOTE);
				ret.Append(uri_encode(pair.Value));
				ret.Append(Constants.OAUTH_QUOTE);
				ret.Append(Constants.OAUTH_KEY_DELIMITER);
				ret.Append(Constants.SPACE);
			}
			//последние два символа уберём
			if (ret.Length>=2) ret.Remove(ret.Length-2,2);
			
			//перед возвратом удалим непостоянное из oauth_content
			oauth_content[Constants.OAUTH_NONCE_KEY]=string.Empty;
			oauth_content[Constants.OAUTH_TIMESTAMP_KEY]=string.Empty;
			oauth_content[Constants.OAUTH_SIGANTURE_KEY]=string.Empty;
			
			return ret.ToString();
		}
		
		private Dictionary<string,string> oauth_content;
		private Random RandomGen=new Random();
		
		private void init_content()
		{
			oauth_content=new Dictionary<string, string>(7);
			oauth_content.Add(Constants.OAUTH_CONSUMER_KEY,string.Empty);
			oauth_content.Add(Constants.OAUTH_NONCE_KEY,string.Empty);
			oauth_content.Add(Constants.OAUTH_SIGANTURE_KEY,string.Empty);
			oauth_content.Add(Constants.OAUTH_SIGNATURE_METHOD_KEY,string.Empty);
			oauth_content.Add(Constants.OAUTH_TIMESTAMP_KEY,string.Empty);
			oauth_content.Add(Constants.OAUTH_TOKEN_KEY,string.Empty);
			oauth_content.Add(Constants.OAUTH_VERSION_KEY,string.Empty);
		}
		
		private void init_constant_values()
		{
			oauth_content[Constants.OAUTH_SIGNATURE_METHOD_KEY]=Constants.OAUTH_SIGNATURE_METHOD_HMACSHA1;
			oauth_content[Constants.OAUTH_VERSION_KEY]=Constants.OAUTH_VERSION_VALUE;
		}
		
		
		/// <summary>
		/// oauth comliant implementation
		/// </summary>
		/// <param name="inp"></param>
		/// <returns></returns>
		private string uri_encode(string inp)
		{
//			StringBuilder res = new StringBuilder();
//
			//            foreach (char symbol in inp)
			//            {
			//                if (Constants.UnreservedChars.IndexOf(symbol) != -1)
			//                {
			//                    res.Append(symbol);
			//                }
			//                else
			//                {
			//                    res.Append('%' + String.Format("{0:X2}", (int)symbol));
			//                }
			//            }
//
			//            return res.ToString();

			
			//именно так всё работае
			return Uri.EscapeDataString(inp);
		}
		
		private string create_signature(Uri uri, string http_method, string body)
		{
			string ret=string.Empty;
			Dictionary<string,string> params_list=new Dictionary<string, string>();
			
			string base_url=uri.Scheme+@"://"+uri.Host+uri.AbsolutePath;
			
			//разбираем параметры в uri
			string uri_query=uri.Query.TrimStart(Constants.URLENCODE_QUESTION_MARK);
			foreach(KeyValuePair<string,string> pair in Helper.EnumNameValueFromUrlEncode(uri_query))
			{
				params_list.Add(Helper.DecodeUrlEncoded(pair.Key),Helper.DecodeUrlEncoded(pair.Value));
			}
			
			//добавляем oauth headers
			foreach(KeyValuePair<string,string> auth_header_param in oauth_content)
			{
				//пропускаем пустые, realm и саму signature
				if((string.IsNullOrEmpty(auth_header_param.Value))||
				   (auth_header_param.Key==Constants.REALM_KEY)||
				   (auth_header_param.Key==Constants.OAUTH_SIGANTURE_KEY))
					continue;
				params_list.Add(Helper.DecodeUrlEncoded(auth_header_param.Key),Helper.DecodeUrlEncoded(auth_header_param.Value));
			}
			
			//доюавляем праметры из body
			//TODO: только если заголовок Content-type xwww-form-urlencoded
			
			foreach(KeyValuePair<string,string> pair in Helper.EnumNameValueFromUrlEncode(body))
			{
				params_list.Add(Helper.DecodeUrlEncoded(pair.Key),Helper.DecodeUrlEncoded(pair.Value));
			}
			
			//и теперь надо кодировать следуя
			// http://tools.ietf.org/html/rfc5849#section-3.6
			//они пишут, что реализация во фреймворках может не вполне соответствовать
			//но будем считать, что Uri перекодирует как нам надо.
			//и сразу сортировать, учитывая, что для твиттера ключи не повторяются
			SortedDictionary<string,string> params_encoded_sorted=new SortedDictionary<string, string>(StringComparer.Ordinal);
			foreach(KeyValuePair<string,string> pair in params_list)
			{
				params_encoded_sorted.Add(uri_encode(pair.Key),uri_encode(pair.Value));
			}
			
			//Теперь составляем paramter string
			/* 
			 * The name of each parameter is concatenated to its corresponding
			 * value using an "=" character (ASCII code 61) as a separator, even
			 * if the value is empty.
			 * The sorted name/value pairs are concatenated together into a
			 * single string by using an "&" character (ASCII code 38) as
			 * separator.
			 */
			string[] param_str_parts=new string[params_encoded_sorted.Count];
			int i=0;
			foreach(KeyValuePair<string,string> pair in params_encoded_sorted)
			{
				param_str_parts[i]=
					pair.Key+
					Constants.URLENCODE_KEYVALUE_DELIMITER+
					pair.Value;
				i++;
			}
			string param_str=string.Join(new String(Constants.URLENCODE_PARAM_DELIMITER,1),param_str_parts);
			
			//Теперь можно составить signature base string
			/*
			 * The HTTP request method in uppercase.  For example: "HEAD",
			 * "GET", "POST", etc.  If the request uses a custom HTTP method, it
			 * MUST be encoded
			 * An "&" character (ASCII code 38).
			 * The base string URI from Section 3.4.1.2, after being encoded
			 * An "&" character (ASCII code 38).
			 * The request parameters as normalized in Section 3.4.1.3.2, after
			 * being encoded
			 */
			string signature_base_string=
				http_method.ToUpper()+
				Constants.URLENCODE_PARAM_DELIMITER+
				uri_encode(base_url)+
				Constants.URLENCODE_PARAM_DELIMITER+
				uri_encode(param_str);
			
			ret=sign_hmac_sha1
				(signature_base_string,
				 uri_encode(ConsumerSecret),
				 uri_encode(TokenSecret));
			
			return ret;
		}
		
		private string sign_hmac_sha1
			(string signature_base_string,
			 string client_shared_secret_encoded,
			 string token_shared_secret_encoded)
		{
			string key_str=
				client_shared_secret_encoded+
				Constants.URLENCODE_PARAM_DELIMITER+
				token_shared_secret_encoded;
			byte[] key_bytes=Encoding.ASCII.GetBytes(key_str);
			byte[] text_bytes=Encoding.ASCII.GetBytes(signature_base_string);
			System.Security.Cryptography.HMACSHA1 hmacsha1=new System.Security.Cryptography.HMACSHA1(key_bytes);
			byte[] digest_bytes=hmacsha1.ComputeHash(text_bytes);
			string ret=Convert.ToBase64String(digest_bytes,Base64FormattingOptions.None);
			return ret;
		}
		
		/// <summary>
		/// The oauth_nonce parameter is a unique token your application should
		///	generate for each unique request. Twitter will use this value
		/// to determine whether a request has been submitted multiple times.
		/// </summary>
		/// <returns></returns>
		private string CreateNonceValue()
		{
			char[] rand_chars=new char[64];
			byte[] rand_bytes=new byte[16];
			RandomGen.NextBytes(rand_bytes);
			int chars_count=Convert.ToBase64CharArray(rand_bytes,0,16,rand_chars,0);
			StringBuilder sb=new StringBuilder();
			for(int i=0;i<chars_count;i++)
			{
				if (Char.IsLetterOrDigit(rand_chars[i])) sb.Append(rand_chars[i]);
			}
			return sb.ToString();
		}
	}
}
